- Follow the FIRST Principles
- Test Only Public Methods
- Name the Test Properly
- Follow the AAA structure
- Avoid Logic in Tests
- Avoid Test Interdependence
- Perform One Act Per Test Method
- Perform One Assertion Per Test Method
- Use Specialized Assertions
- Use Simple Values in Assertions
- Asserting Floating Point Numbers
- Avoid Magic Strings
- Automate Mocks Generation
- Use
setUp()andtearDown()To Control Side-effects - Use the Power of Properties
- Use the Power of Helper Methods
- Use Proper Testing Strategies for UI Testing
- References
- F - Fast:
- Unit tests should take little time to run. Milliseconds.
- I - Isolated (standalone):
- Unit tests should be run in isolation, and have no dependencies (e.g. a file system or database).
- R - Repeatable (deterministic):
- Unit tests should always return the same result if you don't change anything in between runs.
- S - Self-validating:
- Unit tests should be able to automatically detect if they passed or failed. There must be no manual check or human interaction.
- T - Timely:
- Unit tests shouldn't take a disproportionately long time to write compared to the code being tested.
- If you find testing the code taking a large amount of time compared to writing the code, consider a design that is more testable.
- In most cases, there shouldn't be a need to test a private method.
- Private methods are an implementation detail and never exist in isolation.
- Instead, test public methods.
- What you should care about is the end result of the public method that calls into the private one.
- You can validate private methods by unit testing public methods.
- At some point, there's going to be a public facing method that calls the private method as part of its implementation.
Name your tests properly. Use a consistent naming convention.
Example of a good template for unit tests naming convention:
func test_methodName_whenCondition_shouldExpectation {}
// OR
func test_behaviour_whenCondition_shouldExpectation {}
Separate tested functionality from the setup and assertion by using Arrange-Act-Assert (or Given-When-Then):
- Arrange / Given:
- Initialize test data.
- Initialize SUT (system under test).
- Act / When:
- Call tested function (usually on the SUT).
- Capture a result.
- Assert / Then:
- Check that the result matches what is expected.
Example:
func testInvalidEmail() {
// 1. Arrange
let invalidEmail = "ab.com"
let sut = EmailValidator()
// 2. Act
let result = sut.isValid(invalidEmail)
// 3. Assert
XCTAssertFalse(result)
}To minimize the risk of introducing bugs in your unit tests, it's advisable to avoid implementing complex logic within them:
- You can identify the presence of logic by assessing your use of constructs like
if,while,for,switch. - Also, be cautious with operators such as
do-catchwhich can introduce branching logic.
- Each test should handle its own setup and tear down.
- Test dependency occurs when one unit test depends on the outcome of another.
When writing your tests, try to only include one act per test. Multiple acts need to be individually Asserted.
- When the test fails, it is clear which act is failing.
- Ensures that the test is focused on just a single case.
- Gives you the entire picture as to why your tests are failing.
func test_addEmptyEntries_shouldBeTreatedAsZero() {
// Arrange
let sut = StringCalculator()
// Act
let actual1 = sut.add("")
let actual2 = sut.add(",") // ❌
// Assert
XCTAssertEqual(0, actual1)
XCTAssertEqual(0, actual2)
}
func test_addEmptyString_shouldBeTreatedAsZero() {
// Arrange
let sut = StringCalculator()
// Act
let actual = sut.add("") // ✅
// Assert
XCTAssertEqual(expected, actual)
}When writing your tests, try to only include one act per test.
Why?
- When the test fails, it is clear which act is failing.
- Ensures that the test is focused on just a single case.
- Gives you the entire picture as to why your tests are failing.
Use the most specialized assertion available when there is such a choice. Example:
// Equality
XCTAssert(x == y) // ❌
XCTAssertEqual(x, y) // ✅
// Nil and Non-nil
XCTAssert(x != nil) // ❌
XCTAssertNotNil(x) // ✅For instance, when testing square root method, do not use value for which nobody knows the answer.
func testSquareRoot() {
XCTAssertEqual(sqrt(3), 1.73205080757, accuracy: epsilon) // ❌
// Better, we all know that sqrt(4) = 2
XCTAssertEqual(sqrt(4), 2, accuracy: epsilon) // ✅
}Floating point numbers should not be compared for equality. Instead, we should verify that they are almost equal by using some error bound:
let x: Float = 1.48329351
let y: Float = 1.48329351
XCTAssertEqual(x, y) // ❌
let epsilon = 0.0001
XCTAssertEqual(x, y, accuracy: epsilon) // ✅Unit tests shouldn't contain magic strings.
func test_validate_whenPhoneNumberIsInvalid_shouldThrowException() {
let sut = PhoneNumberValidator()
let act = {
try sut.validate("g122345j") // Magic string ❌
}
XCTAssertThrowsError(try act(), "Invalid number error should be thrown") { error in
XCTAssertEqual(error as? PhoneNumberValidator.Error, .invalidNumber)
}
}func test_validate_whenPhoneNumberIsInvalid_shouldThrowException() {
let sut = PhoneNumberValidator()
let INVALID_NUMBER = "g122345j"
let act = {
try sut.validate(INVALID_NUMBER) // ✅
}
XCTAssertThrowsError(try act(), "Invalid number error should be thrown") { error in
XCTAssertEqual(error as? PhoneNumberValidator.Error, .invalidNumber)
}
}Sourcery is a great tool for mocking.
For a given protocol conforming to AutoMockable, it generates mocks that are ready-to-use in your tests. You can check:
- Whether your functions were called.
- The number of times your functions were called.
- The parameters that were passed to the function calls.
- Invoke a closure that takes the passed function parameters.
extension MyProtocol: AutoMockable {}
class MyProtocolMock: MyProtocol {
//MARK: - sayHelloWith
var sayHelloWithNameCallsCount = 0
var sayHelloWithNameCalled: Bool {
return sayHelloWithNameCallsCount > 0
}
var sayHelloWithNameReceivedName: String?
var sayHelloWithNameReceivedInvocations: [String] = []
var sayHelloWithNameClosure: ((String) -> Void)?
func sayHelloWith(name: String) {
sayHelloWithNameCallsCount += 1
sayHelloWithNameReceivedName = name
sayHelloWithNameReceivedInvocations.append(name)
sayHelloWithNameClosure?(name)
}
}There are other useful protocols in Sourcery, for example AutoFixturable which makes creating new instances of an object easier
If a property of a XCTestCase class is shared between unit tests, and our test changes the property, this indirectly affects the whole test suite. To prevent this from happening we must cleanup before and after each test. In our XCTestCase class, override following methods:
override class func setUp() {
// This is the setUp() class method.
// XCTest calls it before calling the first test method.
// Set up any overall initial state here.
}
override class func tearDown() {
// This is the tearDown() class method.
// XCTest calls it after the last test method completes.
// Perform any overall cleanup here.
}// MARK: setUp
override func setUp() async throws {
// This is the setUp() async instance method.
// XCTest calls it before each test method.
// Perform any asynchronous setup in this method.
}
override func setUpWithError() throws {
// This is the setUpWithError() instance method.
// XCTest calls it before each test method.
// Set up any synchronous per-test state that might throw errors here.
}
override func setUp() {
// This is the setUp() instance method.
// XCTest calls it before each test method.
// Set up any synchronous per-test state here.
}
// MARK: tearDown
override func tearDown() {
// This is the tearDown() instance method.
// XCTest calls it after each test method.
// Perform any synchronous per-test cleanup here.
}
override func tearDownWithError() throws {
// This is the tearDownWithError() instance method.
// XCTest calls it after each test method.
// Perform any synchronous per-test cleanup that might throw errors here.
}
override func tearDown() async throws {
// This is the tearDown() async instance method.
// XCTest calls it after each test method.
// Perform any asynchronous per-test cleanup here.
}func testMethod1() throws {
// This is the first test method.
// Your testing code goes here.
addTeardownBlock {
// XCTest executes this when testMethod1() ends.
}
}
func testMethod2() throws {
// This is the second test method.
// Your testing code goes here.
addTeardownBlock {
// XCTest executes this last when testMethod2() ends.
}
addTeardownBlock {
// XCTest executes this first when testMethod2() ends.
}
}You should be careful when initializing SUT (system under test) with all dependencies in a setUp() method. It cane easily result in Test Interdependence which should be avoided.
It's better to create factory method for initializing SUT with different configurations:
class UserStorageTests: XCTestCase {
// MARK: - Helpers
func makeSUT() -> UserStorage {
UserStorage(storage: storageMock, secureStorage: secureStorageMock)
}
func makeSUT(with user: User) -> UserStorage {
let sut = makeSUT()
sut.save(user)
return sut
}
} Extract the duplicated test data and mocks into properties.
class UserStorageTests: XCTestCase {
let userDefaults = UserDefaultsMock() // ✅
let keychain = KeychainMock() // ✅
let user = User(id: 1, username: "U1", password: "P1") // ✅
func testUsernameSavedToStorage() {
makeSUT().save(user)
XCTAssertNotNil(userDefaults.inputUsername)
}
func testPasswordSavedToSecureStorage() {
makeSUT().save(user)
XCTAssertNotNil(keychain.inputPassword)
}
func makeSUT() -> UserStorage {
UserStorage(storage: userDefaults, secureStorage: keychain)
}
}The common strategies of testing UI:
- End-to-end tests (think
XCUI-driven-tests). - Snapshot tests:
- Suited for testing visual layout.
- Not suited for testing the content.
- Testing the content of our UI is straightforward and can be done on low-level with unit tests.
Both kinds of tests are expensive to maintain since the user interface changes quite often. The UI doesn’t have to be tested in an end-to-end fashion.
Strategy for UI testing:
- Use snapshot tests for testing visual layout.
- Use low-level unit tests for testing the content.
- https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices
- https://www.vadimbulavin.com/unit-testing-best-practices-on-ios-with-swift/
- https://www.browserstack.com/guide/unit-testing-best-practices
- https://engineering.theblueground.com/how-we-saved-our-time-with-sourcery-in-ios/
- https://developer.apple.com/documentation/xctest/xctestcase/set_up_and_tear_down_state_in_your_tests
- https://github.com/andredesousa/javascript-unit-testing-best-practices